<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSE请求客户端</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#3B82F6',
                        secondary: '#10B981',
                        danger: '#EF4444',
                        dark: '#1E293B',
                        light: '#F8FAFC'
                    },
                    fontFamily: {
                        sans: ['Inter', 'system-ui', 'sans-serif'],
                    },
                }
            }
        }
    </script>
    <style type="text/tailwindcss">
        @layer utilities {
            .content-auto {
                content-visibility: auto;
            }
            .text-shadow {
                text-shadow: 0 2px 4px rgba(0,0,0,0.1);
            }
            .transition-height {
                transition: max-height 0.3s ease-in-out;
            }
        }
    </style>
</head>
<body class="bg-gray-50 min-h-screen font-sans">
    <div class="container mx-auto px-4 py-8 max-w-6xl">
        <!-- 页面标题 -->
        <header class="mb-8 text-center">
            <h1 class="text-[clamp(1.8rem,4vw,2.5rem)] font-bold text-dark mb-2 text-shadow">
                <i class="fa fa-exchange text-primary mr-2"></i>SSE请求客户端
            </h1>
            <p class="text-gray-600 max-w-2xl mx-auto">
                用于发送Server-Sent Events请求并实时展示响应的工具
            </p>
        </header>

        <main class="bg-white rounded-xl shadow-lg overflow-hidden">
            <!-- 输入区域 -->
            <section class="p-6 border-b border-gray-100">
                <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
                    <i class="fa fa-pencil-square-o text-primary mr-2"></i>请求设置
                </h2>
                
                <div class="space-y-4">
                    <!-- 基础URL输入 -->
                    <div class="grid md:grid-cols-1 gap-2">
                        <label for="basicUri" class="block text-sm font-medium text-gray-700">
                            基础URL (Basic URI)
                        </label>
                        <div class="relative">
                            <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                                <i class="fa fa-globe text-gray-400"></i>
                            </div>
                            <input 
                                type="text" 
                                id="basicUri" 
                                class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-200"
                                placeholder="例如: http://localhost:9102/mcp"
                                value="http://localhost:9102/mcp"
                            >
                        </div>
                    </div>

                    <!-- 端点输入 -->
                    <div class="grid md:grid-cols-1 gap-2">
                        <label for="endpoint" class="block text-sm font-medium text-gray-700">
                            端点 (Endpoint)
                        </label>
                        <div class="relative">
                            <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                                <i class="fa fa-link text-gray-400"></i>
                            </div>
                            <input 
                                type="text" 
                                id="endpoint" 
                                class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-200"
                                placeholder="例如: /message"
                                value="/message"
                            >
                        </div>
                    </div>

                    <!-- 请求参数输入 -->
                    <div class="grid md:grid-cols-1 gap-2">
                        <div class="flex justify-between items-center">
                            <label for="params" class="block text-sm font-medium text-gray-700">
                                请求参数 (JSON格式)
                            </label>
                            <button id="formatBtn" class="text-xs text-primary hover:text-primary/80 flex items-center transition-colors">
                                <i class="fa fa-indent mr-1"></i>格式化
                            </button>
                        </div>
                        <textarea 
                            id="params" 
                            rows="6" 
                            class="block w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-200 font-mono text-sm resize-none"
                            placeholder='例如: {"jsonrpc":"2.0","method":"tools/call","id":"692706ad-3","params":{"name":"demo","arguments":{"name":"蒋颜刚","age":123}}}'
                        >{"jsonrpc": "2.0", "method": "tools/call", "id": "692706ad-3", "params": {"name": "demo", "arguments": {"name": "蒋颜刚", "age": 123}}}</textarea>
                        <p id="paramError" class="text-danger text-xs mt-1 hidden">
                            <i class="fa fa-exclamation-circle mr-1"></i>JSON格式错误，请检查
                        </p>
                    </div>

                    <!-- 控制按钮 -->
                    <div class="flex flex-wrap gap-3 pt-2">
                        <button 
                            id="connectBtn" 
                            class="bg-primary hover:bg-primary/90 text-white px-6 py-2.5 rounded-lg font-medium transition-all duration-200 flex items-center shadow-md hover:shadow-lg transform hover:-translate-y-0.5"
                        >
                            <i class="fa fa-play mr-2"></i>建立连接
                        </button>
                        <button 
                            id="disconnectBtn" 
                            class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2.5 rounded-lg font-medium transition-all duration-200 flex items-center shadow-md hover:shadow-lg transform hover:-translate-y-0.5 opacity-50 cursor-not-allowed"
                            disabled
                        >
                            <i class="fa fa-stop mr-2"></i>断开连接
                        </button>
                        <button 
                            id="clearBtn" 
                            class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-6 py-2.5 rounded-lg font-medium transition-all duration-200 flex items-center shadow-sm hover:shadow transform hover:-translate-y-0.5"
                        >
                            <i class="fa fa-trash mr-2"></i>清空响应
                        </button>
                    </div>

                    <!-- 连接状态 -->
                    <div class="mt-3 flex items-center">
                        <span id="statusIndicator" class="w-3 h-3 rounded-full bg-gray-300 mr-2"></span>
                        <span id="statusText" class="text-sm text-gray-600">未连接</span>
                    </div>
                </div>
            </section>

            <!-- 响应展示区域 -->
            <section class="p-6">
                <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
                    <i class="fa fa-terminal text-primary mr-2"></i>响应内容
                </h2>
                
                <div class="relative">
                    <div id="responseContainer" class="bg-gray-50 border border-gray-200 rounded-lg p-4 h-96 overflow-y-auto font-mono text-sm space-y-4">
                        <div class="text-gray-500 italic">
                            <i class="fa fa-info-circle mr-1"></i>响应将显示在这里...
                        </div>
                    </div>
                    
                    <!-- 复制按钮 -->
                    <button 
                        id="copyBtn" 
                        class="absolute top-3 right-3 bg-white/80 hover:bg-white text-gray-700 p-2 rounded-lg shadow-md transition-all duration-200 opacity-70 hover:opacity-100"
                        title="复制所有响应"
                    >
                        <i class="fa fa-copy"></i>
                    </button>
                </div>
                
                <div id="copyNotification" class="mt-2 text-sm text-secondary hidden">
                    <i class="fa fa-check-circle mr-1"></i>已复制到剪贴板
                </div>
            </section>
        </main>

        <!-- 页脚 -->
        <footer class="mt-8 text-center text-gray-500 text-sm">
            <p>SSE请求客户端 &copy; 2024</p>
        </footer>
    </div>

    <script>
        // DOM元素引用
        const basicUriInput = document.getElementById('basicUri');
        const endpointInput = document.getElementById('endpoint');
        const paramsTextarea = document.getElementById('params');
        const paramError = document.getElementById('paramError');
        const connectBtn = document.getElementById('connectBtn');
        const disconnectBtn = document.getElementById('disconnectBtn');
        const clearBtn = document.getElementById('clearBtn');
        const formatBtn = document.getElementById('formatBtn');
        const responseContainer = document.getElementById('responseContainer');
        const statusIndicator = document.getElementById('statusIndicator');
        const statusText = document.getElementById('statusText');
        const copyBtn = document.getElementById('copyBtn');
        const copyNotification = document.getElementById('copyNotification');

        // SSE连接对象
        let eventSource = null;
        let isConnected = false;

        // 检查JSON格式是否有效
        function isValidJson(str) {
            try {
                JSON.parse(str);
                return true;
            } catch (e) {
                return false;
            }
        }

        // 格式化JSON
        function formatJson(jsonStr) {
            try {
                const parsed = JSON.parse(jsonStr);
                return JSON.stringify(parsed, null, 2);
            } catch (e) {
                return jsonStr;
            }
        }

        // 显示响应
        function showResponse(data, type = 'message') {
            // 创建时间戳
            const timestamp = new Date().toLocaleTimeString();
            
            // 创建响应容器
            const responseElement = document.createElement('div');
            responseElement.className = 'p-3 rounded-lg border-l-4';
            
            // 根据类型设置样式
            if (type === 'message') {
                responseElement.classList.add('bg-blue-50', 'border-blue-400');
            } else if (type === 'error') {
                responseElement.classList.add('bg-red-50', 'border-red-400');
            } else if (type === 'system') {
                responseElement.classList.add('bg-gray-50', 'border-gray-400');
            }
            
            // 添加时间戳
            const timeElement = document.createElement('div');
            timeElement.className = 'text-xs text-gray-500 mb-1';
            timeElement.textContent = `[${timestamp}]`;
            responseElement.appendChild(timeElement);
            
            // 添加内容
            const contentElement = document.createElement('pre');
            contentElement.className = type === 'error' ? 'text-red-700' : 'text-gray-800';
            
            // 尝试格式化JSON
            try {
                const jsonData = JSON.parse(data);
                contentElement.textContent = JSON.stringify(jsonData, null, 2);
            } catch (e) {
                contentElement.textContent = data;
            }
            
            responseElement.appendChild(contentElement);
            
            // 清空初始提示并添加新响应
            if (responseContainer.querySelector('.italic')) {
                responseContainer.innerHTML = '';
            }
            
            responseContainer.appendChild(responseElement);
            
            // 滚动到底部
            responseContainer.scrollTop = responseContainer.scrollHeight;
        }

        // 更新连接状态
        function updateConnectionStatus(connected) {
            isConnected = connected;
            
            if (connected) {
                statusIndicator.className = 'w-3 h-3 rounded-full bg-green-500 mr-2 animate-pulse';
                statusText.textContent = '已连接';
                connectBtn.disabled = true;
                connectBtn.classList.add('opacity-50', 'cursor-not-allowed');
                disconnectBtn.disabled = false;
                disconnectBtn.classList.remove('opacity-50', 'cursor-not-allowed');
                basicUriInput.disabled = true;
                endpointInput.disabled = true;
                paramsTextarea.disabled = true;
                formatBtn.disabled = true;
            } else {
                statusIndicator.className = 'w-3 h-3 rounded-full bg-gray-300 mr-2';
                statusText.textContent = '未连接';
                connectBtn.disabled = false;
                connectBtn.classList.remove('opacity-50', 'cursor-not-allowed');
                disconnectBtn.disabled = true;
                disconnectBtn.classList.add('opacity-50', 'cursor-not-allowed');
                basicUriInput.disabled = false;
                endpointInput.disabled = false;
                paramsTextarea.disabled = false;
                formatBtn.disabled = false;
            }
        }

        // 建立SSE连接
        function connect() {
            // 获取输入值
            const basicUri = basicUriInput.value.trim();
            const endpoint = endpointInput.value.trim();
            const params = paramsTextarea.value.trim();
            
            // 验证输入
            if (!basicUri) {
                alert('请输入基础URL');
                return;
            }
            
            if (!endpoint) {
                alert('请输入端点');
                return;
            }
            
            if (!params) {
                alert('请输入请求参数');
                return;
            }
            
            // 验证JSON格式
            if (!isValidJson(params)) {
                paramError.classList.remove('hidden');
                paramsTextarea.focus();
                return;
            } else {
                paramError.classList.add('hidden');
            }
            
            // 构建完整URL
            const url = basicUri + endpoint;
            
            // 显示连接信息
            showResponse(`正在连接到: ${url}`, 'system');
            
            // 先断开可能存在的连接
            if (eventSource) {
                eventSource.close();
            }
            
            try {
                // 创建SSE连接
                // 注意：SSE通常使用GET方法，但这里需要发送POST数据
                // 因此我们使用fetch API创建POST请求并处理流式响应
                fetch(url, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: params,
                })
                .then(response => {
                    if (!response.ok) {
                        throw new Error(`HTTP错误: ${response.status}`);
                    }

                    // 连接成功
                    updateConnectionStatus(true);
                    showResponse('连接已建立，等待接收数据...', 'system');

                    const reader = response.body.getReader();
                    const decoder = new TextDecoder();

                    // 处理流式响应
                    function readStream() {
                        reader.read().then(({ done, value }) => {
                            if (done) {
                                showResponse('服务器已关闭连接', 'system');
                                updateConnectionStatus(false);
                                return;
                            }

                            // 解码并显示数据
                            const chunk = decoder.decode(value, { stream: true });
                            // 处理SSE格式的数据 (data: ...\n\n)
                            const lines = chunk.split('\n');
                            lines.forEach(line => {
                                if (line.startsWith('data:')) {
                                    const data = line.substring(5).trim();
                                    if (data) {
                                        showResponse(data);
                                    }
                                } else if (line.trim()) {
                                    showResponse(line.trim());
                                }
                            });

                            // 继续读取
                            if (isConnected) {
                                readStream();
                            } else {
                                reader.cancel();
                            }
                        }).catch(error => {
                            showResponse(`读取流时出错: ${error.message}`, 'error');
                            updateConnectionStatus(false);
                        });
                    }

                    readStream();
                })
                .catch(error => {
                    showResponse(`连接失败: ${error.message}`, 'error');
                    updateConnectionStatus(false);
                });

            } catch (error) {
                showResponse(`连接错误: ${error.message}`, 'error');
                updateConnectionStatus(false);
            }
        }

        // 断开SSE连接
        function disconnect() {
            if (eventSource) {
                eventSource.close();
                eventSource = null;
            }
            updateConnectionStatus(false);
            showResponse('已断开连接', 'system');
        }

        // 清空响应
        function clearResponses() {
            responseContainer.innerHTML = `
                <div class="text-gray-500 italic">
                    <i class="fa fa-info-circle mr-1"></i>响应将显示在这里...
                </div>
            `;
        }

        // 复制响应到剪贴板
        function copyResponses() {
            // 收集所有响应文本
            const preElements = responseContainer.querySelectorAll('pre');
            let textToCopy = '';
            
            preElements.forEach((pre, index) => {
                textToCopy += pre.textContent;
                if (index < preElements.length - 1) {
                    textToCopy += '\n\n';
                }
            });
            
            // 复制到剪贴板
            navigator.clipboard.writeText(textToCopy).then(() => {
                // 显示复制成功提示
                copyNotification.classList.remove('hidden');
                setTimeout(() => {
                    copyNotification.classList.add('hidden');
                }, 2000);
            }).catch(err => {
                console.error('无法复制文本: ', err);
                alert('复制失败，请手动复制');
            });
        }

        // 事件监听
        connectBtn.addEventListener('click', connect);
        disconnectBtn.addEventListener('click', disconnect);
        clearBtn.addEventListener('click', clearResponses);
        copyBtn.addEventListener('click', copyResponses);
        
        // 格式化JSON按钮
        formatBtn.addEventListener('click', () => {
            const currentValue = paramsTextarea.value;
            if (currentValue.trim()) {
                const formatted = formatJson(currentValue);
                paramsTextarea.value = formatted;
                
                // 检查格式是否正确
                if (isValidJson(formatted)) {
                    paramError.classList.add('hidden');
                } else {
                    paramError.classList.remove('hidden');
                }
            }
        });
        
        // 输入参数时检查JSON格式
        paramsTextarea.addEventListener('input', () => {
            if (paramsTextarea.value.trim() && !isValidJson(paramsTextarea.value)) {
                paramError.classList.remove('hidden');
            } else {
                paramError.classList.add('hidden');
            }
        });
        
        // 页面关闭时断开连接
        window.addEventListener('beforeunload', () => {
            if (isConnected) {
                disconnect();
            }
        });
    </script>
</body>
</html>
